﻿using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Web;
using System.Web.Mvc;

namespace UICommon.Result
{
    public class ResumableDownloadResult : ActionResult
    {
        #region Constants
        private const string MULTIPART_BOUNDARY = "<q1w2e3r4t5y6u7i8o9p0>";
        private const string MULTIPART_CONTENTTYPE = "multipart/byteranges; boundary=" + MULTIPART_BOUNDARY;
        private const string DEFAULT_CONTENTTYPE = "application/octet-stream";
        private const string HTTP_HEADER_ACCEPT_RANGES = "Accept-Ranges";
        private const string HTTP_HEADER_ACCEPT_RANGES_BYTES = "bytes";
        private const string HTTP_HEADER_ACCEPT_RANGES_NONE = "none";
        private const string HTTP_HEADER_CONTENT_TYPE = "Content-Type";
        private const string HTTP_HEADER_CONTENT_RANGE = "Content-Range";
        private const string HTTP_HEADER_CONTENT_LENGTH = "Content-Length";
        private const string HTTP_HEADER_ENTITY_TAG = "ETag";
        private const string HTTP_HEADER_LAST_MODIFIED = "Last-Modified";
        private const string HTTP_HEADER_RANGE = "Range";
        private const string HTTP_HEADER_IF_RANGE = "If-Range";
        private const string HTTP_HEADER_IF_MATCH = "If-Match";
        private const string HTTP_HEADER_IF_NONE_MATCH = "If-None-Match";
        private const string HTTP_HEADER_IF_MODIFIED_SINCE = "If-Modified-Since";
        private const string HTTP_HEADER_IF_UNMODIFIED_SINCE = "If-Unmodified-Since";
        private const string HTTP_HEADER_UNLESS_MODIFIED_SINCE = "Unless-Modified-Since";
        private const string HTTP_METHOD_GET = "GET";
        private const string HTTP_METHOD_HEAD = "HEAD";

        private const int DEBUGGING_SLEEP_TIME = 0;
        #endregion

        #region Properties
        /// <summary>
        /// Indicates if the HTTP request is for multiple ranges.
        /// </summary>
        public bool IsMultipartRequest { get; private set; }

        /// <summary>
        /// Indicates if the HTTP request is for one or more ranges.
        /// </summary>
        public bool IsRangeRequest { get; private set; }

        /// <summary>
        /// The start byte(s) for the requested range(s).
        /// </summary>
        public long[] StartRangeBytes { get; private set; }

        /// <summary>
        /// The end byte(s) for the requested range(s).
        /// </summary>
        public long[] EndRangeBytes { get; private set; }

        /// <summary>
        /// The size of each chunk of data streamed back to the client.
        /// </summary>
        /// <remarks>
        /// When a client makes a range request the requested file's contents are
        /// read in BufferSize chunks, with each chunk flushed to the output stream
        /// until the requested byte range has been read.
        /// </remarks>
        public virtual int BufferSize { get { return 10240; } }

        /// <summary>
        /// Indicates the path to the log file that records HTTP request and response headers.
        /// </summary>
        /// <remarks>
        /// The log is only enabled when the application is executing in Debug mode.
        /// </remarks>
        public virtual string LogFileName { get { return "~/ResumableFileDownloadHandler.log"; } }

        /// <summary>
        /// Indicates whether Range requests are enabled. If false, the HTTP Handler
        /// ignores the Range HTTP Header and returns the entire contents.
        /// </summary>
        public virtual bool EnableRangeRequests { get { return true; } }

        public bool IsReusable { get { return false; } }

        private Func<HttpContextBase, bool>[] ProcessRequestCheckSteps { get; set; }
        private FileInfo InternalRequestedFileInfo { get; set; }
        private string InternalRequestedFileEntityTag { get; set; }
        private string InternalRequestedFileMimeType { get; set; }
        private NameValueCollection InternalResponseHeaders = new NameValueCollection();
        #endregion

        private FileInfo m_FileInfo = null;
        private String m_FileMimeType = String.Empty;
        public ResumableDownloadResult(FileInfo fileInfo, String fileMimeType)
        {
            m_FileInfo = fileInfo;
            m_FileMimeType = fileMimeType;
            this.ProcessRequestCheckSteps =
            new Func<HttpContextBase, bool>[]
            {
                CheckAuthorizationRules,
                CheckHttpMethod,
                CheckFileRequested,
                CheckRangesRequested,
                CheckIfModifiedSinceHeader,
                CheckIfUnmodifiedSinceHeader,
                CheckIfMatchHeader,
                CheckIfNoneMatchHeader,
                CheckIfRangeHeader
            };
        }

        public override void ExecuteResult(ControllerContext context)
        {
            ProcessRequest(context.HttpContext);
        }

        private void ProcessRequest(HttpContextBase context)
        {


            this.InternalRequestedFileInfo = this.GetRequestedFileInfo();
            this.InternalRequestedFileEntityTag = this.GetRequestedFileEntityTag(context);
            this.InternalRequestedFileMimeType = this.GetRequestedFileMimeType();

            // Parse the Range header (if it exists), populating the StartRangeBytes and EndRangeBytes arrays
            ParseRequestHeaderRanges(context);

            // Perform each check; exit if any check returns false
            foreach (var check in ProcessRequestCheckSteps)
            {
                if (check(context) == false)
                {
#if DEBUG
                    LogResponseHttpHeaders(context.Server.MapPath(this.LogFileName), context.Response);
#endif
                    return;
                }
            }

            if (!this.EnableRangeRequests || !this.IsRangeRequest)
                ReturnEntireEntity(context);
            else
                ReturnPartialEntity(context);
        }

        /// <summary>
        /// 获取请求的文件信息
        /// </summary>
        /// <param name="context"></param>
        /// <returns></returns>
        protected virtual FileInfo GetRequestedFileInfo()
        {
            return m_FileInfo;
            //return new FileInfo(@"C:\SelfStudy\downfile\downfile\c2.mp4");
            //if (System.IO.File.Exists(context.Request.PhysicalPath))
            //    return new FileInfo(context.Request.PhysicalPath);
            //else
            //    return null;
        }

        /// <summary>
        /// 获取请求文件的ETag
        /// </summary>
        /// <param name="context"></param>
        /// <returns></returns>
        protected virtual string GetRequestedFileEntityTag(HttpContextBase context)
        {
            FileInfo requestedFile = this.GetRequestedFileInfo();
            if (requestedFile == null)
                return string.Empty;

            ASCIIEncoding ascii = new ASCIIEncoding();
            byte[] sourceBytes = ascii.GetBytes(
                                    string.Concat(
                                        requestedFile.FullName,
                                        "|",
                                        requestedFile.LastWriteTimeUtc
                                    )
                                 );

            return Convert.ToBase64String(new MD5CryptoServiceProvider().ComputeHash(sourceBytes));
        }

        /// <summary>
        /// 获取请求文件的MIME
        /// </summary>
        /// <param name="context"></param>
        /// <returns></returns>
        protected virtual string GetRequestedFileMimeType()
        {
            return m_FileMimeType;
            //return "video/mp4";
        }

        protected virtual void ParseRequestHeaderRanges(HttpContextBase context)
        {
            HttpRequestBase request = context.Request;
            HttpResponseBase response = context.Response;

            string rangeHeader = RetrieveHeader(request, HTTP_HEADER_RANGE, string.Empty);

            if (string.IsNullOrEmpty(rangeHeader))
            {
                // No Range HTTP Header supplied; send back entire contents
                this.StartRangeBytes = new long[] { 0 };
                this.EndRangeBytes = new long[] { this.InternalRequestedFileInfo.Length - 1 };
                this.IsRangeRequest = false;
                this.IsMultipartRequest = false;
            }
            else
            {
                // rangeHeader contains the value of the Range HTTP Header and can have values like:
                //      Range: bytes=0-1            * Get bytes 0 and 1, inclusive
                //      Range: bytes=0-500          * Get bytes 0 to 500 (the first 501 bytes), inclusive
                //      Range: bytes=400-1000       * Get bytes 500 to 1000 (501 bytes in total), inclusive
                //      Range: bytes=-200           * Get the last 200 bytes
                //      Range: bytes=500-           * Get all bytes from byte 500 to the end
                //
                // Can also have multiple ranges delimited by commas, as in:
                //      Range: bytes=0-500,600-1000 * Get bytes 0-500 (the first 501 bytes), inclusive plus bytes 600-1000 (401 bytes) inclusive

                // Remove "Ranges" and break up the ranges
                string[] ranges = rangeHeader.Replace("bytes=", string.Empty).Split(",".ToCharArray());

                this.StartRangeBytes = new long[ranges.Length];
                this.EndRangeBytes = new long[ranges.Length];

                this.IsRangeRequest = true;
                this.IsMultipartRequest = (this.StartRangeBytes.Length > 1);

                for (int i = 0; i < ranges.Length; i++)
                {
                    const int START = 0, END = 1;

                    // Get the START and END values for the current range
                    string[] currentRange = ranges[i].Split("-".ToCharArray());

                    if (string.IsNullOrEmpty(currentRange[END]))
                        // No end specified
                        this.EndRangeBytes[i] = this.InternalRequestedFileInfo.Length - 1;
                    else
                        // An end was specified
                        this.EndRangeBytes[i] = long.Parse(currentRange[END]);

                    if (string.IsNullOrEmpty(currentRange[START]))
                    {
                        // No beginning specified, get last n bytes of file
                        this.StartRangeBytes[i] = this.InternalRequestedFileInfo.Length - 1 - this.EndRangeBytes[i];
                        this.EndRangeBytes[i] = this.InternalRequestedFileInfo.Length - 1;
                    }
                    else
                    {
                        // A normal begin value
                        this.StartRangeBytes[i] = long.Parse(currentRange[0]);
                    }
                }
            }
        }

        /// <summary>
        /// Adds an HTTP Response Header
        /// </summary>
        /// <remarks>
        /// This method is used to store the Response Headers in a private, member variable,
        /// InternalResponseHeaders, so that the Response Headers may be accesed in the
        /// LogResponseHttpHeaders method, if needed. The Response.Headers property can only
        /// be accessed directly when using IIS 7's Integrated Pipeline mode. This workaround
        /// permits logging of Response Headers when using Classic mode or a web server other
        /// than IIS 7.
        /// </remarks>
        protected void AddHeader(HttpResponseBase response, string name, string value)
        {
            InternalResponseHeaders.Add(name, value);

            response.AddHeader(name, value);
        }

        #region Process Request Step Checks
        /// <summary>
        /// 检查权限
        /// </summary>
        /// <param name="context"></param>
        /// <returns></returns>
        protected virtual bool CheckAuthorizationRules(HttpContextBase context)
        {
            // Any authorization checks should be implemented in the derived class
            return true;
        }

        /// <summary>
        /// 检查http方法
        /// </summary>
        /// <param name="context"></param>
        /// <returns></returns>
        protected virtual bool CheckHttpMethod(HttpContextBase context)
        {
            HttpRequestBase request = context.Request;
            HttpResponseBase response = context.Response;

            if (!request.HttpMethod.Equals(HTTP_METHOD_GET) &&
                !request.HttpMethod.Equals(HTTP_METHOD_HEAD))
            {
                response.StatusCode = 501;  // Not Implemented
                return false;
            }

            return true;
        }

        /// <summary>
        /// 检查文件请求
        /// </summary>
        /// <param name="context"></param>
        /// <returns></returns>
        protected virtual bool CheckFileRequested(HttpContextBase context)
        {
            //HttpRequestBase request = context.Request;
            HttpResponseBase response = context.Response;

            if (this.InternalRequestedFileInfo == null)
            {
                response.StatusCode = 404;  // Not Found
                return false;
            }

            if (this.InternalRequestedFileInfo.Length > int.MaxValue)
            {
                response.StatusCode = 413; // Request Entity Too Large
                return false;
            }

            return true;
        }

        /// <summary>
        /// 检查Ranges
        /// </summary>
        /// <param name="context"></param>
        /// <returns></returns>
        protected virtual bool CheckRangesRequested(HttpContextBase context)
        {

            for (int i = 0; i < this.StartRangeBytes.Length; i++)
            {
                if (this.StartRangeBytes[i] > this.InternalRequestedFileInfo.Length - 1 ||
                    this.EndRangeBytes[i] > this.InternalRequestedFileInfo.Length - 1)
                {
                    context.Response.StatusCode = 400; // Bad Request
                    return false;
                }

                if (this.StartRangeBytes[i] < 0 || this.EndRangeBytes[i] < 0)
                {
                    context.Response.StatusCode = 400; // Bad Request
                    return false;
                }

                if (this.EndRangeBytes[i] < this.StartRangeBytes[i])
                {
                    context.Response.StatusCode = 400; // Bad Request
                    return false;
                }
            }

            return true;
        }

        protected virtual bool CheckIfModifiedSinceHeader(HttpContextBase context)
        {
            HttpRequestBase request = context.Request;
            HttpResponseBase response = context.Response;

            string ifModifiedSinceHeader = RetrieveHeader(request, HTTP_HEADER_IF_MODIFIED_SINCE, string.Empty);

            if (!string.IsNullOrEmpty(ifModifiedSinceHeader))
            {
                // Determine the date
                DateTime ifModifiedSinceDate;
                DateTime.TryParse(ifModifiedSinceHeader, out ifModifiedSinceDate);

                if (ifModifiedSinceDate == DateTime.MinValue)
                    // Could not parse date... do not continue on with check
                    return true;

                DateTime requestedFileModifiedDate = this.InternalRequestedFileInfo.LastWriteTimeUtc;
                requestedFileModifiedDate = new DateTime(
                                                requestedFileModifiedDate.Year,
                                                requestedFileModifiedDate.Month,
                                                requestedFileModifiedDate.Day,
                                                requestedFileModifiedDate.Hour,
                                                requestedFileModifiedDate.Minute,
                                                requestedFileModifiedDate.Second
                                            );
                ifModifiedSinceDate = ifModifiedSinceDate.ToUniversalTime();

                if (requestedFileModifiedDate <= ifModifiedSinceDate)
                {
                    // File was created before specified date
                    response.StatusCode = 304;  // Not Modified
                    return false;
                }
            }

            return true;
        }

        protected virtual bool CheckIfUnmodifiedSinceHeader(HttpContextBase context)
        {
            HttpRequestBase request = context.Request;
            HttpResponseBase response = context.Response;

            string ifUnmodifiedSinceHeader = RetrieveHeader(request, HTTP_HEADER_IF_UNMODIFIED_SINCE, string.Empty);

            if (string.IsNullOrEmpty(ifUnmodifiedSinceHeader))
                // Look for Unless-Modified-Since header
                ifUnmodifiedSinceHeader = RetrieveHeader(request, HTTP_HEADER_UNLESS_MODIFIED_SINCE, string.Empty);

            if (!string.IsNullOrEmpty(ifUnmodifiedSinceHeader))
            {
                // Determine the date
                DateTime ifUnmodifiedSinceDate;
                DateTime.TryParse(ifUnmodifiedSinceHeader, out ifUnmodifiedSinceDate);

                DateTime requestedFileModifiedDate = this.InternalRequestedFileInfo.LastWriteTimeUtc;
                requestedFileModifiedDate = new DateTime(
                                                requestedFileModifiedDate.Year,
                                                requestedFileModifiedDate.Month,
                                                requestedFileModifiedDate.Day,
                                                requestedFileModifiedDate.Hour,
                                                requestedFileModifiedDate.Minute,
                                                requestedFileModifiedDate.Second
                                            );
                if (ifUnmodifiedSinceDate != DateTime.MinValue)
                    ifUnmodifiedSinceDate = ifUnmodifiedSinceDate.ToUniversalTime();

                if (requestedFileModifiedDate > ifUnmodifiedSinceDate)
                {
                    // Could not convert value into date or file was created after specified date
                    response.StatusCode = 412;  // Precondition failed
                    return false;
                }
            }

            return true;
        }

        protected virtual bool CheckIfMatchHeader(HttpContextBase context)
        {
            HttpRequestBase request = context.Request;
            HttpResponseBase response = context.Response;

            string ifMatchHeader = RetrieveHeader(request, HTTP_HEADER_IF_MATCH, string.Empty);

            if (string.IsNullOrEmpty(ifMatchHeader) || ifMatchHeader == "*")
                return true;        // Match found

            // Look for a matching ETag value in ifMatchHeader
            string[] entityIds = ifMatchHeader.Replace("bytes=", string.Empty).Split(",".ToCharArray());

            foreach (string entityId in entityIds)
            {
                if (this.InternalRequestedFileEntityTag == entityId)
                    return true;        // Match found
            }

            // If we reach here, no match found
            response.StatusCode = 412;  // Precondition failed
            return false;
        }

        protected virtual bool CheckIfNoneMatchHeader(HttpContextBase context)
        {
            HttpRequestBase request = context.Request;
            HttpResponseBase response = context.Response;

            string ifNoneMatchHeader = RetrieveHeader(request, HTTP_HEADER_IF_NONE_MATCH, string.Empty);

            if (string.IsNullOrEmpty(ifNoneMatchHeader))
                return true;

            if (ifNoneMatchHeader == "*")
            {
                // Logically invalid request
                response.StatusCode = 412;  // Precondition failed
                return false;
            }

            // Look for a matching ETag value in ifNoneMatchHeader
            string[] entityIds = ifNoneMatchHeader.Replace("bytes=", string.Empty).Split(",".ToCharArray());

            foreach (string entityId in entityIds)
            {
                if (this.InternalRequestedFileEntityTag == entityId)
                {
                    AddHeader(response, HTTP_HEADER_ENTITY_TAG, string.Concat("\"", entityId, "\""));
                    response.StatusCode = 304;  // Not modified
                    return false;        // Match found
                }
            }

            // No match found
            return true;
        }

        protected virtual bool CheckIfRangeHeader(HttpContextBase context)
        {
            HttpRequestBase request = context.Request;
            HttpResponseBase response = context.Response;

            string ifRangeHeader = RetrieveHeader(request, HTTP_HEADER_IF_RANGE, this.InternalRequestedFileEntityTag);

            if (this.IsRangeRequest && ifRangeHeader != this.InternalRequestedFileEntityTag)
            {
                ReturnEntireEntity(context);
                return false;
            }

            return true;
        }
        #endregion

        private string RetrieveHeader(HttpRequestBase request, string headerName, string defaultValue)
        {
            return string.IsNullOrEmpty(request.Headers[headerName]) ? defaultValue : request.Headers[headerName].Replace("\"", string.Empty);
        }

        private void ReturnEntireEntity(HttpContextBase context)
        {
            HttpRequestBase request = context.Request;
            HttpResponseBase response = context.Response;

            response.StatusCode = 200;  // OK
            WriteCommonResponseHeaders(response, this.InternalRequestedFileInfo.Length, this.InternalRequestedFileMimeType);

#if DEBUG
            LogResponseHttpHeaders(context.Server.MapPath(this.LogFileName), response);
            //ReturnChunkedResponse(context);
#endif
            if (request.HttpMethod.Equals(HTTP_METHOD_HEAD) == false)
                response.TransmitFile(this.InternalRequestedFileInfo.FullName);

        }

        private void ReturnPartialEntity(HttpContextBase context)
        {
            HttpRequestBase request = context.Request;
            HttpResponseBase response = context.Response;

            response.StatusCode = 206;  // Partial response

            // Specify the byte range being returned for non-multipart requests
            if (this.IsMultipartRequest == false)
                AddHeader(response, HTTP_HEADER_CONTENT_RANGE,
                                        string.Format("bytes {0}-{1}/{2}",
                                                        this.StartRangeBytes[0].ToString(),
                                                        this.EndRangeBytes[0].ToString(),
                                                        this.InternalRequestedFileInfo.Length.ToString()
                                                      )
                                    );

            WriteCommonResponseHeaders(response,
                                       ComputeContentLength(),
                                       this.IsMultipartRequest ? MULTIPART_CONTENTTYPE : this.InternalRequestedFileMimeType);

#if DEBUG
            LogResponseHttpHeaders(context.Server.MapPath(this.LogFileName), response);
#endif

            if (request.HttpMethod.Equals(HTTP_METHOD_HEAD) == false)
                ReturnChunkedResponse(context);
        }

        private void WriteCommonResponseHeaders(HttpResponseBase response, long contentLength, string contentType)
        {
            AddHeader(response, HTTP_HEADER_CONTENT_LENGTH, contentLength.ToString());
            AddHeader(response, HTTP_HEADER_CONTENT_TYPE, contentType);
            AddHeader(response, HTTP_HEADER_LAST_MODIFIED, this.InternalRequestedFileInfo.LastWriteTimeUtc.ToString("r"));
            AddHeader(response, HTTP_HEADER_ENTITY_TAG, string.Concat("\"", this.InternalRequestedFileEntityTag, "\""));

            if (this.EnableRangeRequests)
                AddHeader(response, HTTP_HEADER_ACCEPT_RANGES, HTTP_HEADER_ACCEPT_RANGES_BYTES);
            else
                AddHeader(response, HTTP_HEADER_ACCEPT_RANGES, HTTP_HEADER_ACCEPT_RANGES_NONE);
        }

        private int ComputeContentLength()
        {
            int contentLength = 0;

            for (int i = 0; i < this.StartRangeBytes.Length; i++)
            {
                contentLength += Convert.ToInt32(this.EndRangeBytes[i] - this.StartRangeBytes[i]) + 1;

                if (this.IsMultipartRequest)
                    contentLength += MULTIPART_BOUNDARY.Length
                                    + this.InternalRequestedFileMimeType.Length
                                    + this.StartRangeBytes[i].ToString().Length
                                    + this.EndRangeBytes[i].ToString().Length
                                    + this.InternalRequestedFileInfo.Length.ToString().Length
                                    + 49;       // Length needed for multipart header
            }

            if (this.IsMultipartRequest)
                contentLength += MULTIPART_BOUNDARY.Length
                                    + 8;    // Length of dash and line break

            return contentLength;
        }

        private void ReturnChunkedResponse(HttpContextBase context)
        {
            HttpResponseBase response = context.Response;

            byte[] buffer = new byte[this.BufferSize];
            using (FileStream fs = this.InternalRequestedFileInfo.OpenRead())
            {
                for (int i = 0; i < this.StartRangeBytes.Length; i++)
                {
                    // Position the stream at the starting byte
                    fs.Seek(this.StartRangeBytes[i], SeekOrigin.Begin);

                    int bytesToReadRemaining = Convert.ToInt32(this.EndRangeBytes[i] - this.StartRangeBytes[i]) + 1;

                    // Output multipart boundary, if needed
                    if (this.IsMultipartRequest)
                    {
                        response.Output.Write("--" + MULTIPART_BOUNDARY);
                        response.Output.Write(string.Format("{0}: {1}", HTTP_HEADER_CONTENT_TYPE, this.InternalRequestedFileMimeType));
                        response.Output.Write(string.Format("{0}: bytes {1}-{2}/{3}",
                                                                HTTP_HEADER_CONTENT_RANGE,
                                                                this.StartRangeBytes[i].ToString(),
                                                                this.EndRangeBytes[i].ToString(),
                                                                this.InternalRequestedFileInfo.Length.ToString()
                                                            )
                                             );
                        response.Output.WriteLine();
                    }

                    // Stream out the requested chunks for the current range request
                    while (bytesToReadRemaining > 0)
                    {
                        if (response.IsClientConnected)
                        {
                            int chunkSize = fs.Read(buffer, 0, this.BufferSize < bytesToReadRemaining ? this.BufferSize : bytesToReadRemaining);
                            response.OutputStream.Write(buffer, 0, chunkSize);

                            bytesToReadRemaining -= chunkSize;

                            response.Flush();

#if DEBUG
                            //System.Threading.Thread.Sleep(DEBUGGING_SLEEP_TIME);
#endif
                        }
                        else
                        {
                            // Client disconnected - quit
                            return;
                        }
                    }
                }

                fs.Close();
            }
        }

        private void LogResponseHttpHeaders(string logFile, HttpResponseBase Response)
        {
            string output = string.Concat("RESPONSE INFORMATION (", Response.StatusCode.ToString(), ")", Environment.NewLine);
            foreach (string name in InternalResponseHeaders.Keys)
            {
                output += string.Format("{0}: {1}{2}", name, InternalResponseHeaders[name], Environment.NewLine);
            }

            output += Environment.NewLine + Environment.NewLine;

            File.AppendAllText(logFile, output);
        }
    }
}
